Example PRO

import { Button, List, Navigation, NavigationStack, Script, Text, useState } from "scripting"

let engine: AVAudioEngine | null = null
let player: AVAudioPlayerNode | null = null
let eq: AVAudioUnitEQ | null = null
let delay: AVAudioUnitDelay | null = null
let reverb: AVAudioUnitReverb | null = null
let buffer: AVAudioPCMBuffer | null = null
let pending: Promise<void> | null = null
let unsubscribe: (() => void) | null = null
let cancelled = false
let alive = true
async function stop() { cancelled = true; await engine?.stop() }
async function release() {
  unsubscribe?.(); unsubscribe = null
  await engine?.stop(); await engine?.dispose(); engine = null
  await player?.dispose(); player = null; await eq?.dispose(); eq = null
  await delay?.dispose(); delay = null; await reverb?.dispose(); reverb = null
  await buffer?.dispose(); buffer = null
  await SharedAudioSession.setActive(false)
}
function Demo() {
  const dismiss = Navigation.useDismiss()
  const [message, setMessage] = useState("Ready · quiet generated tones, no microphone")
  const [busy, setBusy] = useState(false)
  const [ready, setReady] = useState(false)
  const [effects, setEffects] = useState(true)
  const [quiet, setQuiet] = useState(true)
  const report = (text: string) => { if (alive) setMessage(text) }
  async function execute(action: () => Promise<void>) {
    if (pending) return
    setBusy(true)
    pending = action().catch(async error => { report(String(error)); if (alive) setReady(false); await release() }).finally(() => { pending = null; if (alive) setBusy(false) })
    await pending
  }
  async function prepare() {
    await release(); cancelled = false; setReady(false)
    await SharedAudioSession.setCategory("playback", []); await SharedAudioSession.setActive(true)
    engine = await AVAudioEngine.create()
    player = await AVAudioPlayerNode.create(); eq = await AVAudioUnitEQ.create({ numberOfBands: 1 })
    delay = await AVAudioUnitDelay.create(); reverb = await AVAudioUnitReverb.create()
    const format = new AVAudioFormat({ sampleRate: 48000, channelCount: 1 })
    for (const node of [player, eq, delay, reverb]) await engine.attach(node)
    await eq.configureBand(0, { filterType: "lowPass", frequency: 1800, bypass: false })
    await delay.configure({ delayTime: 0.2, feedback: 15, wetDryMix: 18 })
    await reverb.loadFactoryPreset("mediumHall"); await reverb.configure({ wetDryMix: 22 })
    await engine.connect(player, eq, { format }); await engine.connect(eq, delay, { format })
    await engine.connect(delay, reverb, { format }); await engine.connect(reverb, await engine.getMainMixerNode(), { format })
    await player.setMixing({ volume: 0.35 }); setQuiet(true); setEffects(true)
    const samples = new Float32Array(96000)
    for (let n = 0; n < samples.length; n++) {
      const position = n % 24000, frequency = [261.63, 329.63, 392, 523.25][Math.floor(n / 24000)]
      samples[n] = Math.sin(2 * Math.PI * frequency * n / 48000) * 0.15 * Math.exp(-position / 4500)
    }
    buffer = await AVAudioPCMBuffer.fromChannels([samples], format)
    await player.scheduleBuffer(buffer, { loops: true })
    unsubscribe = engine.onEvent(event => {
      if (event.type !== "state") { report(`${event.type}: ${event.state.state}`); if (event.state.state === "needsRebuild") { cancelled = true; setReady(false) } }
    })
    if (cancelled) { await release(); return }
    await engine.start()
    if (cancelled) { await release(); return }
    await player.play(); setReady(true); report("Playing Player → EQ → Delay → Reverb → Mixer. Stop before leaving.")
  }
  async function toggleEffects() {
    const next = !effects; await eq?.setBypass(!next); await delay?.setBypass(!next); await reverb?.setBypass(!next)
    setEffects(next); report(next ? "Effects enabled" : "Effects bypassed")
  }
  return <NavigationStack><List navigationTitle="Audio Effects" toolbar={{ cancellationAction: <Button title="Close" action={dismiss} /> }}>
    <Text>{message}</Text>
    <Button title="Build graph and play melody" action={() => { void execute(prepare) }} disabled={busy} />
    <Button title={effects ? "Bypass effects" : "Enable effects"} action={() => { void execute(toggleEffects) }} disabled={!ready || busy} />
    <Button title={quiet ? "Set volume to 60 percent" : "Set volume to 35 percent"} action={() => { void execute(async () => { await player?.setMixing({ volume: quiet ? 0.6 : 0.35 }); setQuiet(!quiet) }) }} disabled={!ready || busy} />
    <Button title="Pause melody" action={() => { void execute(async () => { await player?.pause(); report("Paused · Resume preserves the player timeline") }) }} disabled={!ready || busy} />
    <Button title="Resume melody" action={() => { void execute(async () => { await player?.play(); report("Playing") }) }} disabled={!ready || busy} />
    <Button title="Stop and release graph" action={() => { cancelled = true; void execute(async () => { await release(); setReady(false); report("Stopped · build a new graph to play again") }) }} disabled={busy} />
  </List></NavigationStack>
}
async function main() {
  try { await Navigation.present({ element: <Demo /> }) }
  finally { try { alive = false; await stop(); await pending; await release(); } finally { Script.exit() } }
}
void main()